package lexer

import (
	"log"

	"gitee.com/chuanjinge/go-interpreter-waiig/src/monkey/tok"
)

// 词法分析

/**
实现：
读取输入的源码，输出对应的词法单元。
暂时实现仅支持ASCII字符，后续改为支持UNICODE。
*/

// 词法单元分析器
type Lexer struct {
	input   string // 完整的输入文本
	pos     int    // 当前扫描到的字节的索引
	nextPos int    // 即将扫描到的字符的索引--实际上在Unicode的读取中这个更有价值，ASCII中该值必然是pos+1
	char    byte   // 当前扫描到的字节的ASCII值(即pos位置的字节)，用作词法单元的首字节
}

// 初始化分析器，预读取首个字节
func New(input string) *Lexer {
	l := &Lexer{input: input}
	l.preReadChar()
	return l
}

// 预读取下一个待处理字节
func (l *Lexer) preReadChar() {
	if l.nextPos >= len(l.input) {
		l.char = 0 // 0作为ASCII码值就是空字节的意思，相当于C中的NULL
	} else {
		l.char = l.input[l.nextPos]
	}

	l.pos = l.nextPos
	l.nextPos += 1
}

// 基于一个字节创建词法单元，适用于运算符、元数据等词法单元
func newByteToken(kind tok.TokenType, char byte) tok.Token {
	return tok.Token{
		Type:    kind,
		Literal: string(char),
	}
}

func newStringToken(kind tok.TokenType, str string) tok.Token {
	return tok.Token{
		Type:    kind,
		Literal: str,
	}
}

// 获取下一个非空字符，注意不更新当前 pos 和 nextpos
func (l *Lexer) PeekNextCh() (byte, int) {
	if l.nextPos >= len(l.input) {
		return 0, l.nextPos
	}
	nextPos := l.nextPos
	for isWhitespace(l.input[nextPos]) {
		nextPos += 1
	}
	return l.input[nextPos], nextPos
}

// 基于当前pos位置的字节开始，分析出一个完整的词法单元(token)
func (l *Lexer) NextToken() tok.Token {
	var token tok.Token

	// 如果当前字节是空字符，忽略，重新预读下一个字节进来开始分析
	l.skipWhitespace()

	// 逐个读取字节，直到符合某一个词法单元的规则，构造称为 token 输出
	switch l.char {
	// !+ [运算符]
	case '=':
		// 有可能是比较操作 == 也有可能是赋值操作 =
		if nextCh, nextPos := l.PeekNextCh(); nextCh == '=' {
			token = newStringToken(tok.EQUAL, "==")
			l.char = '='
			l.pos = nextPos
			l.nextPos = nextPos + 1
		} else {
			token = newByteToken(tok.ASSIGN, l.char)
		}
	case '+':
		token = newByteToken(tok.PLUS, l.char)
	case '-':
		token = newByteToken(tok.SUB, l.char)
	case '*':
		token = newByteToken(tok.MULTI, l.char)
	case '/':
		token = newByteToken(tok.DIVIDE, l.char)
	case '!':
		// 有可能是比较操作 != 也有可能是逻辑非操作 !
		if nextCh, nextPos := l.PeekNextCh(); nextCh == '=' {
			token = newStringToken(tok.NOT_EQ, "!=")
			l.char = '='
			l.pos = nextPos
			l.nextPos = nextPos + 1
		} else {
			token = newByteToken(tok.BANG, l.char)
		}
	case '"':
		// 字符串起始位置
		token = newStringToken(tok.STR, l.readString())
	case '<':
		token = newByteToken(tok.LT, l.char)
	case '>':
		token = newByteToken(tok.GT, l.char)
	// !- [运算符]
	// !+ [分隔符]
	case ';':
		token = newByteToken(tok.SEMICOLON, l.char)
	case ',':
		token = newByteToken(tok.COMMA, l.char)
	case '{':
		token = newByteToken(tok.LBRACE, l.char)
	case '}':
		token = newByteToken(tok.RBRACE, l.char)
	case '(':
		token = newByteToken(tok.LPAREN, l.char)
	case ')':
		token = newByteToken(tok.RPAREN, l.char)
	case '[':
		token = newByteToken(tok.LBRACKET, l.char)
	case ']':
		token = newByteToken(tok.RBRACKET, l.char)
	case ':':
		token = newByteToken(tok.COLON, l.char) // 字典使用
	// !- [分隔符]
	case 0:
		token = newByteToken(tok.EOF, l.char)

	// 不满足如上的词法单元的情况下，需要多字节处理了
	// 剩余的情况可能就是 标识符/关键词/字面量
	default:
		// 连续读取多个字节，注意要跳过前置空白字符（跳过操作在当前方法的入口处处理了!!!），直到碰到后置空白字符。
		// 目前情况下只允许出现大小写字母和下划线（上边的单字节词法单元理论上是不可能在此处出现的）
		// 其他一切字符我们视为非法字符
		if isLetter(l.char) {
			word := l.readIdentifierOrKeyword()
			token.Literal = word
			// 区分一下是 identifier 还是 keyword(let/fn)
			token.Type = tok.AnalyzeWordTokenType(word)
			return token // 此处必须提前return因为已经预读了下一个字节了。如果不return的话，后边还有preReaderChar()这就相当于走了两步
		} else if isDigit(l.char) {
			number := l.readNumber()
			token.Literal = number
			token.Type = tok.INT
			return token
		} else {
			// 无法识别
			log.Printf("非法字符:%+v\n", l.char)
			token = newByteToken(tok.ILLEGAL, l.char)
			return token
		}
	}

	l.preReadChar()
	return token
}

// 从 (引号)" 开始读取字符串直到另一个 (引号)"
func (l *Lexer) readString() string {
	pos := l.pos + 1 // 引号之后的索引坐标
	for {
		// 只要下一个字符不是双引号(同时字符串还没结束)，我们就认为这是在一个字符串之中
		l.preReadChar()
		if l.char == '"' || l.char == 0 {
			break
		}
	}
	return l.input[pos:l.pos] // l.pos 理论上此时已经到了右双引号的位置，或者比结束位置大一的位置
}

func (l *Lexer) skipWhitespace() {
	// 只要当前预读字节是空白字符就一直读下去，直到预读到一个非空字节
	for isWhitespace(l.char) {
		l.preReadChar()
	}
}

// 读取关键词或者标识符
// 调用该方法的时候还无法区分是关键词还是标识符，一并读取，然后精确匹配后区分之
func (l *Lexer) readIdentifierOrKeyword() string {
	// 从前置非空字符开始读到后置空字符
	// 因为每次我们都执行preReadChar进行预读，而预读是会跳过空字符的，所以当前pos位置就是非空字符位置
	posStart := l.pos
	// 如果当前的输出是 a\tb\tc5 的话，其实按照现在的逻辑会把空字符剔除，相当于是对abc5在进行处理，会卡在5这个地方
	for isLetter(l.char) {
		l.preReadChar()
	}
	posEnded := l.pos                 // 这个pos在正常情况下是一个空字符的位置或者EOF的超位置
	return l.input[posStart:posEnded] // 结合slice截取[)左闭右开的性质进行截取操作
}

// 从当前位置开始读取一个数字字符串
// @todo 暂时只支持整数字符串，不支持浮点数、十六进制数、八进制数等等
func (l *Lexer) readNumber() string {
	posStart := l.pos
	for isDigit(l.char) {
		l.preReadChar()
	}
	posEnded := l.pos
	return l.input[posStart:posEnded]
}

// 是否是一个空白字符
func isWhitespace(ch byte) bool {
	return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}

// 是否是一个标识符或者关键字允许的字符（大写字母、小写字母、下划线）
func isLetter(ch byte) bool {
	return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_')
}

// 当前字符是否是一个数字
func isDigit(ch byte) bool {
	return '0' <= ch && ch <= '9'
}
